// (c) 2011 Greg Courville <Greg_Courville@GregLabs.com>

/* This file is part of GLExPIB.

   GLExPIB is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   GLExPIB is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Lesser General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GLExPIB.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <sys/time.h>
#include <sys/io.h>
#include <unistd.h>
#include <malloc.h>
#include <stdio.h>
#include "glexpib.h"

#define DEFAULT_BUS_TIMEOUT 500000
#define DEFAULT_RESP_TIMEOUT 1000000

#define PO_DATA 0
#define PO_STS 1
#define PO_CTL 2
#define O_INPUT 0x20
#define O_DAV 0x01
#define O_ATN 0x02
#define O_RFD 0x04
#define O_NDAC 0x08
#define I_NIFC 0x40
#define I_NREN 0x20
#define I_NEOI 0x10
#define I_NSRQ 0x08
#define M_LIS 0x20
#define M_UNL 0x3f
#define M_TAL 0x40
#define M_UNT 0x5f
#define M_SPE 0x18
#define M_SPD 0x19
#define M_DCL 0x14

#define NAP() usleep(1000)
#define TRY(x) if((r = x)) {debug("ERROR: %d", r); return r;}

#ifdef DEBUG
#include <stdio.h>
#define debug(msg, ...) printf("%d: " msg "\n", __LINE__, ##__VA_ARGS__ )
#else
#define debug(msg, ...)
#endif

struct glexpib_state {
	uint16_t basePort;
	uint8_t masterAddr;
	uint32_t busTimeout;
	uint32_t respTimeout;
	};

const struct {int i; const char *text;} error_text[] = {
	{0, "OK"},
	{GLEXPIB_ERR_PERM, "Access denied"},
	{GLEXPIB_ERR_TIMEOUT, "Bus timeout"},
	{GLEXPIB_ERR_NODEV, "No device connected"},
	{GLEXPIB_ERR_NDAC, "Device failed to accept data"},
	{GLEXPIB_ERR_NOMEM, "Failed to allocate memory"},
	{0, 0}
	};

long usecsSince(struct timeval *ref) {
	struct timeval now;
	gettimeofday(&now, NULL);
	return (now.tv_sec - ref->tv_sec) * 1e6 + (now.tv_usec - ref->tv_usec);
	}

inline int waitCtlLvl(uint16_t basePort, uint8_t m, int high, uint32_t timeout) {
	struct timeval timeRef;
	gettimeofday(&timeRef, NULL);
	while((1&&(inb(basePort + PO_CTL) & m))!=(1&&high)) {
		if(usecsSince(&timeRef) >= timeout) {
			outb(O_INPUT | O_RFD, basePort + PO_CTL);
			return GLEXPIB_ERR_TIMEOUT;
			}
		NAP();
		}
	return 0;
	}

int writeAddress(uint16_t basePort, uint8_t addr, uint32_t timeout) {
	int r;
	outb(O_RFD, basePort + PO_CTL);
	debug("Waiting for RFD high");
	TRY(waitCtlLvl(basePort, O_RFD, 1, timeout));
	outb(O_RFD | O_ATN, basePort + PO_CTL);
	outb(~addr, basePort);
	outb(O_RFD | O_ATN | O_DAV, basePort + PO_CTL);
	debug("Waiting for RFD low");
	TRY(waitCtlLvl(basePort, O_RFD, 0, timeout));
	debug("Waiting for NDAC high");
	TRY(waitCtlLvl(basePort, O_NDAC, 0, timeout));
	outb(O_RFD | O_INPUT, basePort + PO_CTL);
// 	debug("Waiting for NDAC low");
// 	TRY(waitCtlLvl(basePort, O_NDAC, 1));
	debug("Waiting for RFD high");
	TRY(waitCtlLvl(basePort, O_RFD, 1, timeout));
	return 0;
	}

inline int untalk(uint16_t basePort, uint32_t timeout) {
	int r;
	debug("Untalking...");
	TRY(writeAddress(basePort, M_UNT, timeout));
	debug("  Untalked.");
	return 0;
	}

inline int unlisten(uint16_t basePort, uint32_t timeout) {
	int r;
	debug("Unlistening...");
	TRY(writeAddress(basePort, M_UNL, timeout));
	debug("  Unlistened.");
	return 0;
	}

inline int setTalker(uint16_t basePort, uint8_t addr, uint32_t timeout) {
	int r;
	debug("Setting talker to %d...", addr);
	TRY(writeAddress(basePort, M_TAL | addr, timeout));
	debug("  Talker set.");
	return 0;
	}

inline int setListener(uint16_t basePort, uint8_t addr, uint32_t timeout) {
	int r;
	debug("Setting listener to %d...", addr);
	TRY(writeAddress(basePort, M_LIS | addr, timeout));
	debug("  Listener set.");
	return 0;
	}

void glexpib_setRespTimeout(struct glexpib_state *state, uint32_t timeout) {
	state->respTimeout = timeout;
	}

void glexpib_setBusTimeout(struct glexpib_state *state, uint32_t timeout) {
	state->busTimeout = timeout;
	}

int glexpib_init(struct glexpib_state **state, uint16_t basePort, uint8_t masterAddr) {
	int r;
	int failed = 0;
	if(!(*state = malloc(sizeof(struct glexpib_state)))) 
		return GLEXPIB_ERR_NOMEM;
	//TODO: Allow the option to save the port state and restore it on quit
	(*state)->basePort = basePort;
	(*state)->masterAddr = masterAddr;
	(*state)->busTimeout = DEFAULT_BUS_TIMEOUT;
	(*state)->respTimeout = DEFAULT_RESP_TIMEOUT;
	if(ioperm(basePort, 3, 1))
		return GLEXPIB_ERR_PERM;
	outb(0xFF, basePort);
// 	outb(O_NDAC | O_INPUT, basePort + PO_CTL);
	if((r = writeAddress(basePort, M_DCL, (*state)->busTimeout)) == GLEXPIB_ERR_TIMEOUT)
		r = GLEXPIB_ERR_NODEV;
	if(r) {
		free(*state);
		return r;
		}
	return 0;
	}

int writeByte(uint16_t basePort, uint8_t x, uint32_t timeout) {
	int r;
	outb(O_RFD, basePort + PO_CTL);
	TRY(waitCtlLvl(basePort, O_RFD, 1, timeout));
	outb(~x, basePort);
	outb(O_DAV | O_RFD, basePort + PO_CTL);
// 	TRY(waitCtlLvl(basePort, O_RFD, 0));
	TRY(waitCtlLvl(basePort, O_NDAC, 0, timeout));
	outb(O_RFD | O_INPUT, basePort + PO_CTL);
// 	TRY(waitCtlLvl(basePort, O_RFD, 1));
	return 0;
	}

int glexpib_writeString(struct glexpib_state *state, uint8_t addr, char *data, size_t len) {
	int r, i;
	debug("writeString: started");
	TRY(unlisten(state->basePort, state->busTimeout));
	TRY(untalk(state->basePort, state->busTimeout));
	TRY(setTalker(state->basePort, state->masterAddr, state->busTimeout));
	TRY(setListener(state->basePort, addr, state->busTimeout));
	for(i = 0; i < len; ++i) {
		TRY(writeByte(state->basePort, data[i], state->busTimeout));
		debug("writeString: %d bytes written", i+1);
		}
	TRY(unlisten(state->basePort, state->respTimeout));
	TRY(untalk(state->basePort, state->busTimeout));
	debug("writeString: complete");
	return 0;
	}

int readByte(uint16_t basePort, uint8_t *x, uint32_t timeout) {
	int r;
	uint8_t stat;
	outb(O_INPUT | O_RFD | O_NDAC, basePort + PO_CTL);
	TRY(waitCtlLvl(basePort, O_DAV, 1, timeout));
	*x = ~inb(basePort);
	stat = inb(basePort + PO_STS);
	outb(O_INPUT, basePort + PO_CTL);
	TRY(waitCtlLvl(basePort, O_DAV, 0, timeout));
	outb(O_INPUT | O_NDAC, basePort + PO_CTL);
// 	NAP();
// 	outb(O_INPUT | O_RFD | O_NDAC, basePort + PO_CTL);
	if(stat & I_NEOI)
		return 0;
	return -1;
	}

int glexpib_readString(struct glexpib_state *state, uint8_t addr, char *data, size_t *len, size_t max) {
	int r, i;
	debug("readString: started");
	TRY(unlisten(state->basePort, state->respTimeout));
	TRY(untalk(state->basePort, state->busTimeout));
	TRY(setTalker(state->basePort, addr, state->busTimeout));
	TRY(setListener(state->basePort, state->masterAddr, state->busTimeout));
	for(i=0; i < max - 1; ++i) {
		r = readByte(state->basePort, &data[i], state->busTimeout);
		debug("readString: %d bytes read", i+1);
		if((r < 0) || (data[i] == '\n') || (data[i] == 0))
			break;
		else if(r) {
			outb(O_INPUT | O_RFD, state->basePort + PO_CTL);
			debug("readString: aborting on error %d", r);
			return r;
			}
		}
	data[i + 1] = 0;
	*len = i + 2;
	TRY(unlisten(state->basePort, state->respTimeout));
	TRY(untalk(state->basePort, state->busTimeout));
	debug("readString: complete");
	return 0;
	}

void glexpib_quit(struct glexpib_state *state) {
	free(state);
	}

const char *glexpib_strerror(int err) {
	int i = 0;
	while(!(!error_text[i].i && !error_text[i].text)) {
		if(error_text[i].i == err)
			return error_text[i].text;
		++i;
		}
	return NULL;
	}
